/*
 *  Copyright 2017 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

package org.webrtc;

import android.media.MediaCodec;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaFormat;
import android.os.SystemClock;

import androidx.annotation.Nullable;

import android.view.Surface;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;

import org.webrtc.ThreadUtils.ThreadChecker;

/**
 * Android hardware video decoder.
 */
@SuppressWarnings("deprecation")
// Cannot support API 16 without using deprecated methods.
// TODO(sakal): Rename to MediaCodecVideoDecoder once the deprecated implementation is removed.
class AndroidVideoDecoder implements VideoDecoder, VideoSink {
    private static final String TAG = "AndroidVideoDecoder";

    // TODO(magjed): Use MediaFormat.KEY_* constants when part of the public API.
    private static final String MEDIA_FORMAT_KEY_STRIDE = "stride";
    private static final String MEDIA_FORMAT_KEY_SLICE_HEIGHT = "slice-height";
    private static final String MEDIA_FORMAT_KEY_CROP_LEFT = "crop-left";
    private static final String MEDIA_FORMAT_KEY_CROP_RIGHT = "crop-right";
    private static final String MEDIA_FORMAT_KEY_CROP_TOP = "crop-top";
    private static final String MEDIA_FORMAT_KEY_CROP_BOTTOM = "crop-bottom";

    // MediaCodec.release() occasionally hangs.  Release stops waiting and reports failure after
    // this timeout.
    private static final int MEDIA_CODEC_RELEASE_TIMEOUT_MS = 5000;

    // WebRTC queues input frames quickly in the beginning on the call. Wait for input buffers with a
    // long timeout (500 ms) to prevent this from causing the codec to return an error.
    private static final int DEQUEUE_INPUT_TIMEOUT_US = 500000;

    // Dequeuing an output buffer will block until a buffer is available (up to 100 milliseconds).
    // If this timeout is exceeded, the output thread will unblock and check if the decoder is still
    // running.  If it is, it will block on dequeue again.  Otherwise, it will stop and release the
    // MediaCodec.
    private static final int DEQUEUE_OUTPUT_BUFFER_TIMEOUT_US = 100000;

    private final MediaCodecWrapperFactory mediaCodecWrapperFactory;
    private final String codecName;
    private final VideoCodecMimeType codecType;

    private static class FrameInfo {
        final long decodeStartTimeMs;
        final int rotation;

        FrameInfo(long decodeStartTimeMs, int rotation) {
            this.decodeStartTimeMs = decodeStartTimeMs;
            this.rotation = rotation;
        }
    }

    private final BlockingDeque<FrameInfo> frameInfos;
    private int colorFormat;

    // Output thread runs a loop which polls MediaCodec for decoded output buffers.  It reformats
    // those buffers into VideoFrames and delivers them to the callback.  Variable is set on decoder
    // thread and is immutable while the codec is running.
    @Nullable
    private Thread outputThread;

    // Checker that ensures work is run on the output thread.
    private ThreadChecker outputThreadChecker;

    // Checker that ensures work is run on the decoder thread.  The decoder thread is owned by the
    // caller and must be used to call initDecode, decode, and release.
    private ThreadChecker decoderThreadChecker;

    private volatile boolean running;
    @Nullable
    private volatile Exception shutdownException;

    // Dimensions (width, height, stride, and sliceHeight) may be accessed by either the decode thread
    // or the output thread.  Accesses should be protected with this lock.
    private final Object dimensionLock = new Object();
    private int width;
    private int height;
    private int stride;
    private int sliceHeight;

    // Whether the decoder has finished the first frame.  The codec may not change output dimensions
    // after delivering the first frame.  Only accessed on the output thread while the decoder is
    // running.
    private boolean hasDecodedFirstFrame;
    // Whether the decoder has seen a key frame.  The first frame must be a key frame.  Only accessed
    // on the decoder thread.
    private boolean keyFrameRequired;

    private final @Nullable
    EglBase.Context sharedContext;
    // Valid and immutable while the decoder is running.
    @Nullable
    private SurfaceTextureHelper surfaceTextureHelper;
    @Nullable
    private Surface surface;

    private static class DecodedTextureMetadata {
        final long presentationTimestampUs;
        final Integer decodeTimeMs;

        DecodedTextureMetadata(long presentationTimestampUs, Integer decodeTimeMs) {
            this.presentationTimestampUs = presentationTimestampUs;
            this.decodeTimeMs = decodeTimeMs;
        }
    }

    // Metadata for the last frame rendered to the texture.
    private final Object renderedTextureMetadataLock = new Object();
    @Nullable
    private DecodedTextureMetadata renderedTextureMetadata;

    // Decoding proceeds asynchronously.  This callback returns decoded frames to the caller.  Valid
    // and immutable while the decoder is running.
    @Nullable
    private Callback callback;

    // Valid and immutable while the decoder is running.
    @Nullable
    private MediaCodecWrapper codec;

    AndroidVideoDecoder(MediaCodecWrapperFactory mediaCodecWrapperFactory, String codecName,
                        VideoCodecMimeType codecType, int colorFormat, @Nullable EglBase.Context sharedContext) {
        if (!isSupportedColorFormat(colorFormat)) {
            throw new IllegalArgumentException("Unsupported color format: " + colorFormat);
        }
        Logging.d(TAG,
                "ctor name: " + codecName + " type: " + codecType + " color format: " + colorFormat
                        + " context: " + sharedContext);
        this.mediaCodecWrapperFactory = mediaCodecWrapperFactory;
        this.codecName = codecName;
        this.codecType = codecType;
        this.colorFormat = colorFormat;
        this.sharedContext = sharedContext;
        this.frameInfos = new LinkedBlockingDeque<>();
    }

    @Override
    public VideoCodecStatus initDecode(Settings settings, Callback callback) {
        this.decoderThreadChecker = new ThreadChecker();

        this.callback = callback;
        if (sharedContext != null) {
            surfaceTextureHelper = createSurfaceTextureHelper();
            surface = new Surface(surfaceTextureHelper.getSurfaceTexture());
            surfaceTextureHelper.startListening(this);
        }
        return initDecodeInternal(settings.width, settings.height);
    }

    // Internal variant is used when restarting the codec due to reconfiguration.
    private VideoCodecStatus initDecodeInternal(int width, int height) {
        decoderThreadChecker.checkIsOnValidThread();
        Logging.d(TAG,
                "initDecodeInternal name: " + codecName + " type: " + codecType + " width: " + width
                        + " height: " + height);
        if (outputThread != null) {
            Logging.e(TAG, "initDecodeInternal called while the codec is already running");
            return VideoCodecStatus.FALLBACK_SOFTWARE;
        }

        // Note:  it is not necessary to initialize dimensions under the lock, since the output thread
        // is not running.
        this.width = width;
        this.height = height;

        stride = width;
        sliceHeight = height;
        hasDecodedFirstFrame = false;
        keyFrameRequired = true;

        try {
            codec = mediaCodecWrapperFactory.createByCodecName(codecName);
        } catch (IOException | IllegalArgumentException | IllegalStateException e) {
            Logging.e(TAG, "Cannot create media decoder " + codecName);
            return VideoCodecStatus.FALLBACK_SOFTWARE;
        }
        try {
            MediaFormat format = MediaFormat.createVideoFormat(codecType.mimeType(), width, height);
            if (sharedContext == null) {
                format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
            }
            codec.configure(format, surface, null, 0);
            codec.start();
        } catch (IllegalStateException | IllegalArgumentException e) {
            Logging.e(TAG, "initDecode failed", e);
            release();
            return VideoCodecStatus.FALLBACK_SOFTWARE;
        }
        running = true;
        outputThread = createOutputThread();
        outputThread.start();

        Logging.d(TAG, "initDecodeInternal done");
        return VideoCodecStatus.OK;
    }

    @Override
    public VideoCodecStatus decode(EncodedImage frame, DecodeInfo info) {
        decoderThreadChecker.checkIsOnValidThread();
        if (codec == null || callback == null) {
            Logging.d(TAG, "decode uninitalized, codec: " + (codec != null) + ", callback: " + callback);
            return VideoCodecStatus.UNINITIALIZED;
        }

        if (frame.buffer == null) {
            Logging.e(TAG, "decode() - no input data");
            return VideoCodecStatus.ERR_PARAMETER;
        }

        int size = frame.buffer.remaining();
        if (size == 0) {
            Logging.e(TAG, "decode() - input buffer empty");
            return VideoCodecStatus.ERR_PARAMETER;
        }

        // Load dimensions from shared memory under the dimension lock.
        final int width;
        final int height;
        synchronized (dimensionLock) {
            width = this.width;
            height = this.height;
        }

        // Check if the resolution changed and reset the codec if necessary.
        if (frame.encodedWidth * frame.encodedHeight > 0
                && (frame.encodedWidth != width || frame.encodedHeight != height)) {
            VideoCodecStatus status = reinitDecode(frame.encodedWidth, frame.encodedHeight);
            if (status != VideoCodecStatus.OK) {
                return status;
            }
        }

        if (keyFrameRequired) {
            // Need to process a key frame first.
            if (frame.frameType != EncodedImage.FrameType.VideoFrameKey) {
                Logging.e(TAG, "decode() - key frame required first");
                return VideoCodecStatus.NO_OUTPUT;
            }
        }

        int index;
        try {
            index = codec.dequeueInputBuffer(DEQUEUE_INPUT_TIMEOUT_US);
        } catch (IllegalStateException e) {
            Logging.e(TAG, "dequeueInputBuffer failed", e);
            return VideoCodecStatus.ERROR;
        }
        if (index < 0) {
            // Decoder is falling behind.  No input buffers available.
            // The decoder can't simply drop frames; it might lose a key frame.
            Logging.e(TAG, "decode() - no HW buffers available; decoder falling behind");
            return VideoCodecStatus.ERROR;
        }

        ByteBuffer buffer;
        try {
            buffer = codec.getInputBuffers()[index];
        } catch (IllegalStateException e) {
            Logging.e(TAG, "getInputBuffers failed", e);
            return VideoCodecStatus.ERROR;
        }

        if (buffer.capacity() < size) {
            Logging.e(TAG, "decode() - HW buffer too small");
            return VideoCodecStatus.ERROR;
        }
        buffer.put(frame.buffer);

        frameInfos.offer(new FrameInfo(SystemClock.elapsedRealtime(), frame.rotation));
        try {
            codec.queueInputBuffer(index, 0 /* offset */, size,
                    TimeUnit.NANOSECONDS.toMicros(frame.captureTimeNs), 0 /* flags */);
        } catch (IllegalStateException e) {
            Logging.e(TAG, "queueInputBuffer failed", e);
            frameInfos.pollLast();
            return VideoCodecStatus.ERROR;
        }
        if (keyFrameRequired) {
            keyFrameRequired = false;
        }
        return VideoCodecStatus.OK;
    }

    @Override
    public String getImplementationName() {
        return codecName;
    }

    @Override
    public VideoCodecStatus release() {
        // TODO(sakal): This is not called on the correct thread but is still called synchronously.
        // Re-enable the check once this is called on the correct thread.
        // decoderThreadChecker.checkIsOnValidThread();
        Logging.d(TAG, "release");
        VideoCodecStatus status = releaseInternal();
        if (surface != null) {
            releaseSurface();
            surface = null;
            surfaceTextureHelper.stopListening();
            surfaceTextureHelper.dispose();
            surfaceTextureHelper = null;
        }
        synchronized (renderedTextureMetadataLock) {
            renderedTextureMetadata = null;
        }
        callback = null;
        frameInfos.clear();
        return status;
    }

    // Internal variant is used when restarting the codec due to reconfiguration.
    private VideoCodecStatus releaseInternal() {
        if (!running) {
            Logging.d(TAG, "release: Decoder is not running.");
            return VideoCodecStatus.OK;
        }
        try {
            // The outputThread actually stops and releases the codec once running is false.
            running = false;
            if (!ThreadUtils.joinUninterruptibly(outputThread, MEDIA_CODEC_RELEASE_TIMEOUT_MS)) {
                // Log an exception to capture the stack trace and turn it into a TIMEOUT error.
                Logging.e(TAG, "Media decoder release timeout", new RuntimeException());
                return VideoCodecStatus.TIMEOUT;
            }
            if (shutdownException != null) {
                // Log the exception and turn it into an error.  Wrap the exception in a new exception to
                // capture both the output thread's stack trace and this thread's stack trace.
                Logging.e(TAG, "Media decoder release error", new RuntimeException(shutdownException));
                shutdownException = null;
                return VideoCodecStatus.ERROR;
            }
        } finally {
            codec = null;
            outputThread = null;
        }
        return VideoCodecStatus.OK;
    }

    private VideoCodecStatus reinitDecode(int newWidth, int newHeight) {
        decoderThreadChecker.checkIsOnValidThread();
        VideoCodecStatus status = releaseInternal();
        if (status != VideoCodecStatus.OK) {
            return status;
        }
        return initDecodeInternal(newWidth, newHeight);
    }

    private Thread createOutputThread() {
        return new Thread("AndroidVideoDecoder.outputThread") {
            @Override
            public void run() {
                outputThreadChecker = new ThreadChecker();
                while (running) {
                    deliverDecodedFrame();
                }
                releaseCodecOnOutputThread();
            }
        };
    }

    // Visible for testing.
    protected void deliverDecodedFrame() {
        outputThreadChecker.checkIsOnValidThread();
        try {
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            // Block until an output buffer is available (up to 100 milliseconds).  If the timeout is
            // exceeded, deliverDecodedFrame() will be called again on the next iteration of the output
            // thread's loop.  Blocking here prevents the output thread from busy-waiting while the codec
            // is idle.
            int result = codec.dequeueOutputBuffer(info, DEQUEUE_OUTPUT_BUFFER_TIMEOUT_US);
            if (result == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                reformat(codec.getOutputFormat());
                return;
            }

            if (result < 0) {
                Logging.v(TAG, "dequeueOutputBuffer returned " + result);
                return;
            }

            FrameInfo frameInfo = frameInfos.poll();
            Integer decodeTimeMs = null;
            int rotation = 0;
            if (frameInfo != null) {
                decodeTimeMs = (int) (SystemClock.elapsedRealtime() - frameInfo.decodeStartTimeMs);
                rotation = frameInfo.rotation;
            }

            hasDecodedFirstFrame = true;

            if (surfaceTextureHelper != null) {
                deliverTextureFrame(result, info, rotation, decodeTimeMs);
            } else {
                deliverByteFrame(result, info, rotation, decodeTimeMs);
            }

        } catch (IllegalStateException e) {
            Logging.e(TAG, "deliverDecodedFrame failed", e);
        }
    }

    private void deliverTextureFrame(final int index, final MediaCodec.BufferInfo info,
                                     final int rotation, final Integer decodeTimeMs) {
        // Load dimensions from shared memory under the dimension lock.
        final int width;
        final int height;
        synchronized (dimensionLock) {
            width = this.width;
            height = this.height;
        }

        synchronized (renderedTextureMetadataLock) {
            if (renderedTextureMetadata != null) {
                codec.releaseOutputBuffer(index, false);
                return; // We are still waiting for texture for the previous frame, drop this one.
            }
            surfaceTextureHelper.setTextureSize(width, height);
            surfaceTextureHelper.setFrameRotation(rotation);
            renderedTextureMetadata = new DecodedTextureMetadata(info.presentationTimeUs, decodeTimeMs);
            codec.releaseOutputBuffer(index, /* render= */ true);
        }
    }

    @Override
    public void onFrame(VideoFrame frame) {
        final VideoFrame newFrame;
        final Integer decodeTimeMs;
        final long timestampNs;
        synchronized (renderedTextureMetadataLock) {
            if (renderedTextureMetadata == null) {
                throw new IllegalStateException(
                        "Rendered texture metadata was null in onTextureFrameAvailable.");
            }
            timestampNs = renderedTextureMetadata.presentationTimestampUs * 1000;
            decodeTimeMs = renderedTextureMetadata.decodeTimeMs;
            renderedTextureMetadata = null;
        }
        // Change timestamp of frame.
        final VideoFrame frameWithModifiedTimeStamp =
                new VideoFrame(frame.getBuffer(), frame.getRotation(), timestampNs);
        callback.onDecodedFrame(frameWithModifiedTimeStamp, decodeTimeMs, null /* qp */);
    }

    private void deliverByteFrame(
            int result, MediaCodec.BufferInfo info, int rotation, Integer decodeTimeMs) {
        // Load dimensions from shared memory under the dimension lock.
        int width;
        int height;
        int stride;
        int sliceHeight;
        synchronized (dimensionLock) {
            width = this.width;
            height = this.height;
            stride = this.stride;
            sliceHeight = this.sliceHeight;
        }

        // Output must be at least width * height bytes for Y channel, plus (width / 2) * (height / 2)
        // bytes for each of the U and V channels.
        if (info.size < width * height * 3 / 2) {
            Logging.e(TAG, "Insufficient output buffer size: " + info.size);
            return;
        }

        if (info.size < stride * height * 3 / 2 && sliceHeight == height && stride > width) {
            // Some codecs (Exynos) report an incorrect stride.  Correct it here.
            // Expected size == stride * height * 3 / 2.  A bit of algebra gives the correct stride as
            // 2 * size / (3 * height).
            stride = info.size * 2 / (height * 3);
        }

        ByteBuffer buffer = codec.getOutputBuffers()[result];
        buffer.position(info.offset);
        buffer.limit(info.offset + info.size);
        buffer = buffer.slice();

        final VideoFrame.Buffer frameBuffer;
        if (colorFormat == CodecCapabilities.COLOR_FormatYUV420Planar) {
            frameBuffer = copyI420Buffer(buffer, stride, sliceHeight, width, height);
        } else {
            // All other supported color formats are NV12.
            frameBuffer = copyNV12ToI420Buffer(buffer, stride, sliceHeight, width, height);
        }
        codec.releaseOutputBuffer(result, /* render= */ false);

        long presentationTimeNs = info.presentationTimeUs * 1000;
        VideoFrame frame = new VideoFrame(frameBuffer, rotation, presentationTimeNs);

        // Note that qp is parsed on the C++ side.
        callback.onDecodedFrame(frame, decodeTimeMs, null /* qp */);
        frame.release();
    }

    private VideoFrame.Buffer copyNV12ToI420Buffer(
            ByteBuffer buffer, int stride, int sliceHeight, int width, int height) {
        // toI420 copies the buffer.
        return new NV12Buffer(width, height, stride, sliceHeight, buffer, null /* releaseCallback */)
                .toI420();
    }

    private VideoFrame.Buffer copyI420Buffer(
            ByteBuffer buffer, int stride, int sliceHeight, int width, int height) {
        if (stride % 2 != 0) {
            throw new AssertionError("Stride is not divisible by two: " + stride);
        }

        // Note that the case with odd |sliceHeight| is handled in a special way.
        // The chroma height contained in the payload is rounded down instead of
        // up, making it one row less than what we expect in WebRTC. Therefore, we
        // have to duplicate the last chroma rows for this case. Also, the offset
        // between the Y plane and the U plane is unintuitive for this case. See
        // http://bugs.webrtc.org/6651 for more info.
        final int chromaWidth = (width + 1) / 2;
        final int chromaHeight = (sliceHeight % 2 == 0) ? (height + 1) / 2 : height / 2;

        final int uvStride = stride / 2;

        final int yPos = 0;
        final int yEnd = yPos + stride * height;
        final int uPos = yPos + stride * sliceHeight;
        final int uEnd = uPos + uvStride * chromaHeight;
        final int vPos = uPos + uvStride * sliceHeight / 2;
        final int vEnd = vPos + uvStride * chromaHeight;

        VideoFrame.I420Buffer frameBuffer = allocateI420Buffer(width, height);

        buffer.limit(yEnd);
        buffer.position(yPos);
        copyPlane(
                buffer.slice(), stride, frameBuffer.getDataY(), frameBuffer.getStrideY(), width, height);

        buffer.limit(uEnd);
        buffer.position(uPos);
        copyPlane(buffer.slice(), uvStride, frameBuffer.getDataU(), frameBuffer.getStrideU(),
                chromaWidth, chromaHeight);
        if (sliceHeight % 2 == 1) {
            buffer.position(uPos + uvStride * (chromaHeight - 1)); // Seek to beginning of last full row.

            ByteBuffer dataU = frameBuffer.getDataU();
            dataU.position(frameBuffer.getStrideU() * chromaHeight); // Seek to beginning of last row.
            dataU.put(buffer); // Copy the last row.
        }

        buffer.limit(vEnd);
        buffer.position(vPos);
        copyPlane(buffer.slice(), uvStride, frameBuffer.getDataV(), frameBuffer.getStrideV(),
                chromaWidth, chromaHeight);
        if (sliceHeight % 2 == 1) {
            buffer.position(vPos + uvStride * (chromaHeight - 1)); // Seek to beginning of last full row.

            ByteBuffer dataV = frameBuffer.getDataV();
            dataV.position(frameBuffer.getStrideV() * chromaHeight); // Seek to beginning of last row.
            dataV.put(buffer); // Copy the last row.
        }

        return frameBuffer;
    }

    private void reformat(MediaFormat format) {
        outputThreadChecker.checkIsOnValidThread();
        Logging.d(TAG, "Decoder format changed: " + format.toString());
        final int newWidth;
        final int newHeight;
        if (format.containsKey(MEDIA_FORMAT_KEY_CROP_LEFT)
                && format.containsKey(MEDIA_FORMAT_KEY_CROP_RIGHT)
                && format.containsKey(MEDIA_FORMAT_KEY_CROP_BOTTOM)
                && format.containsKey(MEDIA_FORMAT_KEY_CROP_TOP)) {
            newWidth = 1 + format.getInteger(MEDIA_FORMAT_KEY_CROP_RIGHT)
                    - format.getInteger(MEDIA_FORMAT_KEY_CROP_LEFT);
            newHeight = 1 + format.getInteger(MEDIA_FORMAT_KEY_CROP_BOTTOM)
                    - format.getInteger(MEDIA_FORMAT_KEY_CROP_TOP);
        } else {
            newWidth = format.getInteger(MediaFormat.KEY_WIDTH);
            newHeight = format.getInteger(MediaFormat.KEY_HEIGHT);
        }
        // Compare to existing width, height, and save values under the dimension lock.
        synchronized (dimensionLock) {
            if (newWidth != width || newHeight != height) {
                if (hasDecodedFirstFrame) {
                    stopOnOutputThread(new RuntimeException("Unexpected size change. "
                            + "Configured " + width + "*" + height + ". "
                            + "New " + newWidth + "*" + newHeight));
                    return;
                } else if (newWidth <= 0 || newHeight <= 0) {
                    Logging.w(TAG,
                            "Unexpected format dimensions. Configured " + width + "*" + height + ". "
                                    + "New " + newWidth + "*" + newHeight + ". Skip it");
                    return;
                }
                width = newWidth;
                height = newHeight;
            }
        }

        // Note:  texture mode ignores colorFormat.  Hence, if the texture helper is non-null, skip
        // color format updates.
        if (surfaceTextureHelper == null && format.containsKey(MediaFormat.KEY_COLOR_FORMAT)) {
            colorFormat = format.getInteger(MediaFormat.KEY_COLOR_FORMAT);
            Logging.d(TAG, "Color: 0x" + Integer.toHexString(colorFormat));
            if (!isSupportedColorFormat(colorFormat)) {
                stopOnOutputThread(new IllegalStateException("Unsupported color format: " + colorFormat));
                return;
            }
        }

        // Save stride and sliceHeight under the dimension lock.
        synchronized (dimensionLock) {
            if (format.containsKey(MEDIA_FORMAT_KEY_STRIDE)) {
                stride = format.getInteger(MEDIA_FORMAT_KEY_STRIDE);
            }
            if (format.containsKey(MEDIA_FORMAT_KEY_SLICE_HEIGHT)) {
                sliceHeight = format.getInteger(MEDIA_FORMAT_KEY_SLICE_HEIGHT);
            }
            Logging.d(TAG, "Frame stride and slice height: " + stride + " x " + sliceHeight);
            stride = Math.max(width, stride);
            sliceHeight = Math.max(height, sliceHeight);
        }
    }

    private void releaseCodecOnOutputThread() {
        outputThreadChecker.checkIsOnValidThread();
        Logging.d(TAG, "Releasing MediaCodec on output thread");
        try {
            codec.stop();
        } catch (Exception e) {
            Logging.e(TAG, "Media decoder stop failed", e);
        }
        try {
            codec.release();
        } catch (Exception e) {
            Logging.e(TAG, "Media decoder release failed", e);
            // Propagate exceptions caught during release back to the main thread.
            shutdownException = e;
        }
        Logging.d(TAG, "Release on output thread done");
    }

    private void stopOnOutputThread(Exception e) {
        outputThreadChecker.checkIsOnValidThread();
        running = false;
        shutdownException = e;
    }

    private boolean isSupportedColorFormat(int colorFormat) {
        for (int supported : MediaCodecUtils.DECODER_COLOR_FORMATS) {
            if (supported == colorFormat) {
                return true;
            }
        }
        return false;
    }

    // Visible for testing.
    protected SurfaceTextureHelper createSurfaceTextureHelper() {
        return SurfaceTextureHelper.create("decoder-texture-thread", sharedContext);
    }

    // Visible for testing.
    // TODO(sakal): Remove once Robolectric commit fa991a0 has been rolled to WebRTC.
    protected void releaseSurface() {
        surface.release();
    }

    // Visible for testing.
    protected VideoFrame.I420Buffer allocateI420Buffer(int width, int height) {
        return JavaI420Buffer.allocate(width, height);
    }

    // Visible for testing.
    protected void copyPlane(
            ByteBuffer src, int srcStride, ByteBuffer dst, int dstStride, int width, int height) {
        YuvHelper.copyPlane(src, srcStride, dst, dstStride, width, height);
    }
}
